winbrew_core\fs\archive\extract/
gzip.rs

1use std::fs;
2use std::io::{self, Read, Write};
3use std::path::{Path, PathBuf};
4
5use flate2::read::GzDecoder;
6
7use crate::fs::{FsError, Result};
8
9pub(crate) fn extract_gzip_archive(gzip_path: &Path, destination_dir: &Path) -> Result<()> {
10    let file = fs::File::open(gzip_path).map_err(|err| FsError::open_archive(gzip_path, err))?;
11    let mut decoder = GzDecoder::new(file);
12
13    fs::create_dir_all(destination_dir)
14        .map_err(|err| FsError::create_directory(destination_dir, err))?;
15
16    let output_path = output_path_for_gzip_archive(gzip_path, destination_dir)?;
17    let temp_output_path = temporary_output_path_for(&output_path);
18    let mut output_file = fs::File::create(&temp_output_path)
19        .map_err(|err| FsError::create_extracted_file(&temp_output_path, err))?;
20
21    let copy_result = copy_gzip_contents(
22        gzip_path,
23        &output_path,
24        &temp_output_path,
25        &mut decoder,
26        &mut output_file,
27    );
28
29    drop(output_file);
30
31    if let Err(err) = copy_result {
32        let _ = fs::remove_file(&temp_output_path);
33        return Err(err);
34    }
35
36    fs::rename(&temp_output_path, &output_path)
37        .map_err(|err| FsError::finalize_file(&temp_output_path, &output_path, err))?;
38
39    Ok(())
40}
41
42fn copy_gzip_contents(
43    gzip_path: &Path,
44    output_path: &Path,
45    temp_output_path: &Path,
46    decoder: &mut GzDecoder<fs::File>,
47    output_file: &mut fs::File,
48) -> Result<()> {
49    let mut buffer = [0u8; 8 * 1024];
50
51    loop {
52        let bytes_read = decoder
53            .read(&mut buffer)
54            .map_err(|err| FsError::read_archive_entry(gzip_path, err))?;
55
56        if bytes_read == 0 {
57            break;
58        }
59
60        output_file
61            .write_all(&buffer[..bytes_read])
62            .map_err(|err| FsError::write_entry(output_path, err))?;
63    }
64
65    output_file
66        .sync_all()
67        .map_err(|err| FsError::sync_temp_file(temp_output_path, err))?;
68
69    Ok(())
70}
71
72fn output_path_for_gzip_archive(gzip_path: &Path, destination_dir: &Path) -> Result<PathBuf> {
73    let file_name = gzip_path
74        .file_name()
75        .and_then(|name| name.to_str())
76        .ok_or_else(|| {
77            FsError::open_archive(
78                gzip_path,
79                io::Error::new(
80                    io::ErrorKind::InvalidInput,
81                    "gzip archive path has no file name",
82                ),
83            )
84        })?;
85
86    let lower_file_name = file_name.to_ascii_lowercase();
87    if !lower_file_name.ends_with(".gz") {
88        return Err(FsError::open_archive(
89            gzip_path,
90            io::Error::new(
91                io::ErrorKind::InvalidInput,
92                "gzip archive path does not end with .gz",
93            ),
94        ));
95    }
96
97    let output_name = &file_name[..file_name.len() - 3];
98    if output_name.is_empty() {
99        return Err(FsError::open_archive(
100            gzip_path,
101            io::Error::new(
102                io::ErrorKind::InvalidInput,
103                "gzip archive path resolves to an empty output name",
104            ),
105        ));
106    }
107
108    Ok(destination_dir.join(output_name))
109}
110
111fn temporary_output_path_for(output_path: &Path) -> PathBuf {
112    let file_name = output_path
113        .file_name()
114        .and_then(|name| name.to_str())
115        .unwrap_or("output");
116
117    output_path.with_file_name(format!("{file_name}.tmp"))
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::fs;
124    use std::io::Write;
125    use tempfile::tempdir;
126
127    fn create_gz_archive(path: &std::path::Path, contents: &[u8]) {
128        let file = fs::File::create(path).expect("create gz file");
129        let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
130
131        encoder.write_all(contents).expect("write gz contents");
132        encoder.finish().expect("finish gz file");
133    }
134
135    #[test]
136    fn extract_gzip_archive_extracts_file() {
137        let temp_dir = tempdir().expect("temp dir");
138        let destination_dir = temp_dir.path().join("dest");
139        let archive_path = temp_dir.path().join("tool.exe.gz");
140
141        fs::create_dir_all(&destination_dir).expect("destination dir");
142        create_gz_archive(&archive_path, b"gzip payload");
143
144        extract_gzip_archive(&archive_path, &destination_dir).expect("gzip extraction");
145
146        assert_eq!(
147            fs::read(destination_dir.join("tool.exe")).expect("read"),
148            b"gzip payload"
149        );
150    }
151}